﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Diagnostics;

namespace Xceed.Silverlight.ListBox.PositioningFunction
{
  internal class PathPositioning : PositioningFunctionBase
  {
    #region Contructor
    internal PathPositioning( List<Point> pointsOnFlattenedPath,
      Rect pathBounds,
      Thickness padding,
      Stretch stretch,
      ContainerOrientation containerOrientation,
      double containerPadding,
      double startOffset )
    {
      this.Stretch = stretch;
      this.ContainerOrientation = containerOrientation;
      this.ContainerPadding = containerPadding;
      this.StartOffset = startOffset;
      this.PropertyChanged += new PropertyChangedEventHandler( OnPropertyChanged );

      m_padding = padding;

      // We mut keep the path's bound to be able to fit all the containers into the viewport 
      m_pointsOnFlattenedPath = pointsOnFlattenedPath;
      m_pathBound = pathBounds;

    }
    #endregion

    #region DisplayName Property

    internal override string DisplayName
    {
      get
      {
        return "Path Positioning function";
      }
    }

    #endregion

    #region Padding Property

    internal Thickness Padding
    {
      get
      {
        return m_padding;
      }
      set
      {
        if( m_padding != value )
        {
          m_padding = value;
          this.NotifyPropertyChanged( "Padding" );
        }
      }
    }

    private Thickness m_padding;

    #endregion

    #region ContainerPadding

    internal double ContainerPadding
    {
      get;
      private set;
    }

    #endregion

    #region Stretch Property

    internal Stretch Stretch
    {
      get;
      set;
    }

    #endregion

    #region ContainerOrientation Property

    internal ContainerOrientation ContainerOrientation
    {
      get;
      private set;
    }

    #endregion

    #region StartOffset Property

    public double StartOffset
    {
      get;
      private set;
    }

    #endregion

    #region ViewportCachedSize Property

    private Size ViewportCahcedSize
    {
      get
      {
        return m_viewportCachedSize;
      }

      set
      {
        if( m_viewportCachedSize != value )
        {
          m_viewportCachedSize = value;
          this.NotifyPropertyChanged( "ViewportCachedSize" );
        }
      }
    }

    #endregion

    #region Internal Methods
    internal override void PrepareLayoutPass( double partialOffset, Size itemContainerSize, Size groupContainerSize )
    {
      m_firstContainerLayouted = false;
      m_cachedPartialOffset = partialOffset;
      m_cachedItemContainerSize = itemContainerSize;
    }

    internal override void PlaceContainerForward( Size container,
      bool isGroupContainer,
      out Point containerPosition, 
      out double containerProportionInViewport,
      out int layoutProgressionPercentage,
      out double rotateAngle )
    {
      containerProportionInViewport = 1.0d;
      containerPosition = new Point();
      rotateAngle = 0.0d;

      double containerSpace = container.Width + this.ContainerPadding;

      if( !m_firstContainerLayouted )
      {
        double pixelsPartialOffset = 0.0;
        if( !isGroupContainer )
        {
          pixelsPartialOffset = m_cachedPartialOffset * containerSpace;
        }
        else
        {
          pixelsPartialOffset = m_cachedPartialOffset * ( containerSpace + m_cachedItemContainerSize.Width + this.ContainerPadding);
        }
        double startOffsetAbs = this.StartOffset * (this.EffectiveViewportEnd - this.EffectiveViewportStart);
        this.IndexPositionTopBottom = startOffsetAbs + this.StartOffsetIndex - pixelsPartialOffset;
        this.IndexPositionBottomUp = startOffsetAbs + this.StartOffsetIndex - pixelsPartialOffset;
        m_firstContainerLayouted = true;
      }

      double distanceFromPathBeginning = this.IndexPositionTopBottom - this.ExtendedViewportStart;

      this.GetPositionForDistance( distanceFromPathBeginning, m_fittingMatrix, out containerPosition, out rotateAngle );

      layoutProgressionPercentage = ( int )( ( distanceFromPathBeginning / m_pathTotalLength ) * 100.0d );

      containerProportionInViewport = this.GetContainerProportionInEffectiveViewport( container, new Point( this.IndexPositionTopBottom, 0.0d ) );

      this.IndexPositionTopBottom += ( container.Width + this.ContainerPadding );

    }

    internal override void PlaceContainerBackward( Size container,
     bool isGroupContainer,
     out Point containerPosition,
     out double containerProportionInViewport,
     out int layoutProgressionPercentage,
     out double rotateAngle )
    {
      containerProportionInViewport = 1.0d;
      containerPosition = new Point();
      rotateAngle = 0.0d;

      this.IndexPositionBottomUp -= ( container.Width + this.ContainerPadding );

      double distanceFromPathBeginning = this.IndexPositionBottomUp - this.ExtendedViewportStart;

      this.GetPositionForDistance( distanceFromPathBeginning, m_fittingMatrix, out containerPosition, out rotateAngle );

      layoutProgressionPercentage = ( int )( ( distanceFromPathBeginning / m_pathTotalLength ) * 100.0d );

      containerProportionInViewport = this.GetContainerProportionInEffectiveViewport( container, new Point( this.IndexPositionBottomUp, 0.0d ) );

    }

    internal override Point GetBringIntoViewPositionForContainer( Size groupHeaderContainerSize,
      Size itemContainerSize,
      Size viewportSize,
      bool forwardScrolling )
    {
      throw new NotSupportedException( "This method shoudn't be call for the path positionning" );
    }

    internal override double GetScrollOffsetDifferenceBetweenPositions( Point originalPosition, Point targetPosition, Size containerSize )
    {
      throw new NotSupportedException( "This method shoudn't be call for the path positionning" );
    }

    internal override Rect GetCorrectedViewportVisibilityRect( Size stickyHeaderSize, Rect viewportVisibilityRect )
    {
      // The sticky header dosen't have any effect on the 
      // viewport visibility rectangle for a path positioning layout
      return viewportVisibilityRect;
    }

    internal override void UpdateEffectiveViewportSizeInPixels( Size newViewportSizeInPixels )
    {
      this.ViewportCahcedSize = newViewportSizeInPixels;
      if( m_fittingMatrix == null )
      {
        this.UpdateComputedPath();
      }

      double effectivePathLength = m_pathTotalLength / 1.50;
      double oversizedInPixels = m_pathTotalLength - effectivePathLength;
      double nbPixelsToAddEachSide = oversizedInPixels / 2.0d;

      this.EffectiveViewportStart = 0.0d;
      this.EffectiveViewportEnd = effectivePathLength;

      this.ExtendedViewportStart = this.EffectiveViewportStart - nbPixelsToAddEachSide;
      this.ExtendedViewportEnd = this.EffectiveViewportEnd + nbPixelsToAddEachSide;

      this.StartOffsetIndex = this.EffectiveViewportStart;

      this.ResetPositioningIndexes();
    }

    internal override void CorrectViewportSize( double itemsInViewport, ref Size viewportSize )
    {
      // This is because we want every objet to be layout once at the startoffset position.
      // Accodring to the scrollviewer definition, the StartOffset represent the viewport. And
      // this viewport has the size of 1 item.
      viewportSize.Height = 1.0d;
    }

    internal override Point PlaceStickyHeader( Point lastContainerOfTheGroupPosition, Size containerSize, out double angle )
    {
      Point stickyPostion;
      double distanceFromBeginning = this.EffectiveViewportStart - this.ExtendedViewportStart;
      this.GetPositionForDistance( distanceFromBeginning, m_fittingMatrix, out stickyPostion, out angle );
      return stickyPostion;
    }

    #endregion

    #region Protected Internal Methods
    protected internal override double GetContainerProportionInEffectiveViewport( Size containerSize, Point containerPosition )
    {
      // 5 possible cases 
      // #1 container is completly out of the effective viewport (at the top) (0.0)
      // #2 container is completly out of the effective viewport (at the end) (0.0)
      // #3 container is completly in the effective viewport (1.0)
      // #4 container is partially in the effective viewport (at the top) (between 0.0 and 1.0)
      // #5 container is partially in the effective viewport (at the end) (between 0.0 and 1.0)
      double containerDistance = containerSize.Width + this.ContainerPadding;
      double containerStartPosition = containerPosition.X;
      double containerEndPosition = containerPosition.X + containerDistance;
      double sizeOfContainerInViewport = 0.0d;

      if( containerStartPosition < this.EffectiveViewportStart )
      {
        // Case #1 or #4
        if( containerEndPosition < this.EffectiveViewportStart )
        {
          // Case #1
          return 0.0d;
        }
        else
        {
          // Case #4
          sizeOfContainerInViewport = containerEndPosition - this.EffectiveViewportStart;
          return sizeOfContainerInViewport / containerDistance;
        }
      }
      else if( containerEndPosition > this.EffectiveViewportEnd )
      {
        // Case #2 or #5
        if( containerStartPosition > this.EffectiveViewportEnd )
        {
          // Case #2
          return 0.0d;
        }
        else
        {
          // Case #5
          sizeOfContainerInViewport = this.EffectiveViewportEnd - containerStartPosition;
          return sizeOfContainerInViewport / containerDistance;
        }
      }
      else
      {
        // Case 3
        return 1.0;
      }
    }

    internal override double GetGroupHeaderProportion()
    {
      throw new NotSupportedException( "This method shoudn't be call for the path positionning" );
    }

    internal override Size GetCorrectedContainerSize( Size containerSize, Size viewportSize )
    {
      return new Size( containerSize.Width, containerSize.Height );
    }

    #endregion

    #region Private Methods
    private void OnPropertyChanged( object sender, PropertyChangedEventArgs e )
    {
      if( e.PropertyName == "ViewportCachedSize" )
      {
        this.UpdateComputedPath();
      }
    }

    private double GetPathTotalLength( MatrixTransform fitToViewport )
    {
      Point startPoint = fitToViewport.Transform( m_pointsOnFlattenedPath[ 0 ] );
      int count = m_pointsOnFlattenedPath.Count;
      Point currentPoint = startPoint;
      Point nextPoint;
      double totalPathDistance = 0.0d;

      // Make sure we have at least to points
      if( count >= 2 )
      {
        for( int i = 1; i < count; i++ )
        {
          nextPoint = fitToViewport.Transform( m_pointsOnFlattenedPath[ i ] );

          if( nextPoint == currentPoint )
          {
            currentPoint = nextPoint;
            continue;
          }

          // Compute the distance between the current point and the next point
          totalPathDistance +=
            Math.Sqrt( Math.Pow( ( nextPoint.Y - currentPoint.Y ), 2 )
                        + Math.Pow( ( nextPoint.X - currentPoint.X ), 2 ) );

          currentPoint = nextPoint;
        }
      }

      return totalPathDistance;
    }

    private bool GetPositionForDistance( double targetDistanceFromPathBeginning,
      MatrixTransform fitToViewport,
      out Point position,
      out double angle )
    {
      bool foundAPosition = false;
      double actualDistanceFromBeginning = 0;
      double nextDistanceFromBeginning = 0;
      double targetDisplacement = 0;
      Point startPoint = fitToViewport.Transform( m_pointsOnFlattenedPath[ 0 ] );
      int count = m_pointsOnFlattenedPath.Count;
      Point currentPoint = startPoint;
      Point nextPoint;
      position = PositioningFunctionBase.InvalidPosition;
      angle = 0.0d;

      // Make sure we have at least to points
      if( count < 2 )
        return foundAPosition;

      for( int i = 1; i < count; i++ )
      {
        nextPoint = fitToViewport.Transform( m_pointsOnFlattenedPath[ i ] );

        if( nextPoint == currentPoint )
        {
          currentPoint = nextPoint;
          continue;
        }

        // Compute the distance between the current point and the next point
        double distance = Math.Sqrt( Math.Pow( ( nextPoint.Y - currentPoint.Y ), 2 ) + Math.Pow( ( nextPoint.X - currentPoint.X ), 2 ) );
        nextDistanceFromBeginning += distance;

        // If the actualDistanceFromBeginning is greater than the
        // targetDistanceFromPathBeginning
        if( nextDistanceFromBeginning >= targetDistanceFromPathBeginning )
        {
          // We must find the target distance on this segment
          targetDisplacement = targetDistanceFromPathBeginning - actualDistanceFromBeginning;

          // We must split the last segment into intersections of
          // distanceBetweenPoints distance
          double deltaX = ( nextPoint.X - currentPoint.X );
          double deltaY = ( nextPoint.Y - currentPoint.Y );

          double slope = deltaY / deltaX;
          double theta = Math.Atan( slope );

          double ratio = targetDisplacement / distance;
          double x = ratio * deltaX;
          double y = ratio * deltaY;

          // Cmopute the exact position of the container
          position = new Point( currentPoint.X + x, currentPoint.Y + y );


          if( this.ContainerOrientation == ContainerOrientation.OrientOnPath )
          {
            if( ( deltaX >= 0 ) && ( deltaY >= 0 ) )
            {
              angle = ( 180.0d * Math.Atan( slope ) / Math.PI );
            }
            else if( ( deltaX < 0 ) && ( deltaY >= 0 ) )
            {
              angle = ( 180.0d * ( Math.PI + Math.Atan( slope ) ) / Math.PI );
            }
            else if( ( deltaX < 0 ) && ( deltaY < 0 ) )
            {
              angle = ( 180.0d * ( Math.PI + Math.Atan( slope ) ) / Math.PI );
            }
            else if( ( deltaX >= 0 ) && ( deltaY < 0 ) )
            {
              angle = ( 180.0d * Math.Atan( slope ) / Math.PI );
            }
          }

          foundAPosition = true;
          break;

        }
        else
        {
          actualDistanceFromBeginning += distance;
        }

        currentPoint = nextPoint;
      }

      return foundAPosition;
    }

    private MatrixTransform GetFittingMatrix( Size viewport, Stretch stretch )
    {
      double scaleX = 1d;
      double scaleY = 1d;
      double viewportHeightDifferentialOffset = 0d;
      double viewportWidthDifferentialOffset = 0d;
      double offsetX = m_padding.Left;
      double offsetY = m_padding.Top;

      if( stretch != Stretch.None )
      {
        scaleX = ( viewport.Width - ( m_padding.Left + m_padding.Right ) ) / m_pathBound.Width;
        scaleY = ( viewport.Height - ( m_padding.Top + m_padding.Bottom ) ) / m_pathBound.Height;

        switch( stretch )
        {
          case Stretch.Fill:
            offsetX = m_padding.Left - ( m_pathBound.X * scaleX );
            offsetY = m_padding.Top - ( m_pathBound.Y * scaleY );
            break;

          case Stretch.Uniform:

            // Ensure to scale on both axis
            // using the same scale factor
            if( scaleX < scaleY )
            {
              scaleY = scaleX;
              double correctedViewportHeight = viewport.Height - ( m_padding.Top + m_padding.Bottom );
              viewportHeightDifferentialOffset = ( correctedViewportHeight - ( m_pathBound.Height * scaleY ) ) / 2;
              offsetX = m_padding.Left - ( m_pathBound.X * scaleX );
              offsetY = m_padding.Top + viewportHeightDifferentialOffset - ( m_pathBound.Y * scaleY );
            }
            else
            {
              scaleX = scaleY;
              double correctedViewportWidth = viewport.Width - ( m_padding.Left + m_padding.Right );
              viewportWidthDifferentialOffset = ( correctedViewportWidth - ( m_pathBound.Width * scaleX ) ) / 2;
              offsetX = m_padding.Left + viewportWidthDifferentialOffset - ( m_pathBound.X * scaleX );
              offsetY = m_padding.Top - ( m_pathBound.Y * scaleY );
            }

            break;
          case Stretch.UniformToFill:

            // Ensure to scale to 1 the axis
            // requiring the highest scaling
            // factor and scale the other axis
            // proportionnaly to the previously
            // computed scale factors.
            if( scaleX < scaleY )
            {
              scaleX = scaleY;
              offsetX = m_padding.Left - ( m_pathBound.X * scaleX );
              offsetY = m_padding.Top - ( m_pathBound.Y * scaleY );
            }
            else
            {
              scaleY = scaleX;
              offsetX = m_padding.Left - ( m_pathBound.X * scaleX );
              offsetY = m_padding.Top - ( m_pathBound.Y * scaleY );
            }
            break;
        }
      }
      else
      {
        offsetX = m_padding.Left + ( m_pathBound.X * scaleX );
        offsetY = m_padding.Top + ( m_pathBound.Y * scaleY );
      }

      Matrix transformationMatrix = new Matrix( scaleX, 0, 0, scaleY, offsetX, offsetY );
      MatrixTransform matrixTransform = new MatrixTransform();
      matrixTransform.Matrix = transformationMatrix;

      return matrixTransform;
    }

    private void UpdateComputedPath()
    {
      m_fittingMatrix = this.GetFittingMatrix( m_viewportCachedSize, this.Stretch );

      m_pathTotalLength = this.GetPathTotalLength( m_fittingMatrix );
    }

    #endregion

    #region Private Members
    // Path positioning members
    private List<Point> m_pointsOnFlattenedPath;
    private Rect m_pathBound = new Rect( new Point( 0, 0 ), Size.Empty );
    private double m_pathTotalLength = 0.0d;
    MatrixTransform m_fittingMatrix = null;

    // Layout state members
    private Size m_viewportCachedSize = Size.Empty;
    private bool m_firstContainerLayouted = false;
    private double m_cachedPartialOffset = 0.0d;
    private Size m_cachedItemContainerSize = new Size( 0.0d, 0.0d );
    #endregion
  }
}
